HTTP 2 笔记

March 24, 2021

前言

续网络请求后,学习 HTTP/2。HTTP/2 的主要协议 RFC 7540 出来已经三年多了,HTTP/2 的应用也越来越多。几年前还在为 HTTPS 响应速度慢,能不上 HTTPS 就不上,结果现在基本成为标配了。对于大型系统,HTTP/2 也会是这样,毕竟知乎都是 HTTP 2 了。

HTTP/1.1

从 HTTP/1.1 诞生至今,互联网已经走过了十几个年头,当初的 HTTP/1.1 协议出现了不少缺陷,其中管线化,就是其中一个突出问题。

上图中的绿色是数据量的增加,红色是资源数的增加。流行的网站其首页加载的资源越来越多,在 2015 年的时候已经达到 2.1M 的数据量了,同时更严重的是完成渲染需要 100 多个资源,与 HTTP/1.1 出来时候的互联网环境与其相比,非常吓人,。

在 HTTP 请求时候,一个 TCP 连接上只能同时有一个请求/响应,如果你一次要发送多个请求,那可以建立多个 TCP 连接来实现,但是 主流浏览器只能允许同域下 6~8 个连接。这意味着如果你想同时访问多个连接怎么办,只能等,等最快的那个 HTTP 响应返回,通过 Keep-Alive 的方式继续使用该 TCP socket,而不是再次进行断开,握手的操作。在日益复杂的资源数下,管线化问题就日益突出了。

管线化是什么?看看维基中的图:

这张图很清晰的指出了,管线化可以避免在一个 TCP 连接下需要等待服务端响应,才能继续发送请求的问题。这个技术 HTTP pipelining 将多个请求批量提交,其实在 HTTP/1.1 中已经实现了,但是由于线头阻塞(Head of line blocking)的问题,服务器对浏览器的响应是按照其请求顺序来的,如果前一个响应出现阻塞,后一个请求将不会被处理。这样一来 HTTP pipelining 并没有解决 Head of line blocking 的问题。大部分浏览器也关闭了这项管线化功能。

下图比较全面的概况 HTTP/1.1 中的缺陷:

对于多请求的问题,前端可以采用雪碧图、js 文件合并、图片资源内联 base64,接口合并等等的方式。而后端可以采用分片的形式,简而言之即使既然限制了 6~8 个连接,那就用更多的主机,散列域名,比如微博的:wx1.sinaimg.cn,wx2.sinaimg.cn,wx3.sinaimg.cnwx4.sinaimg.cn。这样简单暴力,可以提升载入速度。

只是对于下面提到的服务端问题,请求/响应头的问题,已经不在 HTTP/1.1 能够处理范围了。

HTTP/2

HTTP/2 相比与 HTTP/1.1 并不会破坏现有的工作,服务器和客户端都必须确定自己是否完整兼容http2或者彻底不兼容。HTTP/2标准于2015年5月以 RFC 7540 正式发表,主要基于 Google 的 SPDY 协议。

先总结一下 HTTP/2 的新特性

  1. 二进制分帧
  2. 多路复用的流
  3. 优先级/依赖性
  4. 服务器推送
  5. 头部压缩

这样上面就是其主要特性,下面着重介绍一下

二进制分帧

HTTP/2 是二进制协议,而 HTTP/1.1 是文本/ascii的协议,这也是其根本性的不同,是 HTTP/2 性能增强的核心。

该图就很清晰说明了一切,对 HTTP 和 TCP 都没有改动,只是在中间添加了一个层 二进制分帧层。HTTP/1.1 采用换行符作为纯文本的分隔符,而在 HTTP/2 里面则是将所有信息分割为更小单元的消息和帧的形式。用二进制的方式传输,这也是 HTTP/1.1 和 HTTP/2 的根本区别。

在这个二进制分帧层下,我们看看数据的传输是什么样子的:

上面就是数据传输的过程,可以看到数据是由 流(stream) 形成的,在 stream 里面有一个个 消息(message),而 message 里面还有 帧(frame)。在第一条流里面,有两个消息,请求消息和响应消息。响应消息里面又有两帧,分别是 HEADERS 和 DATA 帧,是 HTTP/2 里面最常见的帧。

上文图里面的消息,由与逻辑请求或响应消息对应的完整的一系列帧组成的,但是这些帧却可以不用整整齐齐排在一起,可以和其他的消息的帧交叉分散在一个流里面,从而实现交错分散,交错的帧也不会产生相互的干扰影响。再在服务器/客户端里面组装好。二进制帧的交错发送实现了请求响应的并行,解决了线头阻塞的问题

这是一个官方的示例,well,在相同的网速下,可以看到左侧 HTTP/1.1 的图片是接近于一排一排的刷出来的,而右侧 HTTP/2 则是 duang 一下就好了。HTTP/2 真是强无敌,还有什么理由不用 HTTP/2 呢。

二进制的帧

下图是 HTTP/2 帧的通用结构:

+-----------------------------------------------+
|                Length (24)                    |
+---------------+---------------+---------------+
|  Type (8)     |  Flags (8)    |
+-+-------------+---------------+-------------------------------+
|R|                Stream Identifier (31)                       |
+=+=============================================================+
|                  Frame Payload (0...)                       ...
+---------------------------------------------------------------+

可以看到一共有 9 个字节,变化的部分是帧负载(payload),其余的 9 个字节是固定的。从上到下分别是

  1. 帧长度 Length:24位表示帧负债的长度,默认最大长度是 2^14,当超过此值的时候,将不允许发送,除非收到接收方的长度修改同通知。
  2. 帧类型 Type:8位表示帧的类型,HTTP/2 规范定义了 10 种帧,包括:HEADER(0x1),DATA(0x0),SETTINGS(0x4),CONTINUATION(0x9),PUSH_PROMISE(0x5),PING(0x8),PRIORITY(0x2),WINDOW_UPDATE(0x8),RST_STREAWM(0x3),GOAWAY(0x7)。这里有最常见的 HEADER 和 DATA 帧。
  3. 帧标识 Flags:8位表示帧的标识,一个帧可以同时是有多个帧标识。也和发送的帧类型数据有关系。
  4. 帧保留位 R:在 HTTP/2 下作为保留字段。
  5. 流标识符 Stream Identifier

HTTP/2 允许帧扩展的,毕竟有 8 位长度。

双方可以在逐跳原则(hop-by-hop basis) 基础上协商使用新的帧,但这些帧的状态无法被改变,也不受流控制.

常见的包括 ALTSVC(Alternative Services):替换服务,服务端建议客户端连接到另外一台服务器,实现负债均衡;BLOCKED:阻塞,用于通知接收方,因为流量控制无法发送数据。

多路复用的流

提到多路复用,有通信领域常见的时分复用,空分复用亦或则是频复用等等,HTTP/2 里面的多路复用,更像是时分复用。

一个 TCP 连接里面可以允许多个流的存在,而流本身是由自己的 31 位的标识符的。客户端创建的流是奇数,服务端创建的流是偶数,

流的多路复用意味着在同一连接中来自各个流的数据包会被混合在一起。就好像两个(或者更多) 独立的“数据列车”被拼凑到了一辆列车上,但它们最终会在终点站被分开。

多路复用的实现是二进制上的。

优先性与依赖性

流的优先级在于告诉对方哪个流更重要,分配多少资源。优先级取决于数据流的权重以及依赖关系

流与流之间是存在依赖关系的,所有流默认依赖 “根数据流” 0x0,其权重值在 1 至 256 之间的整数。同父级流的同样权重的流具有相同的优先级,而不是由顺序决定。可以看看下图:

HTTP/2 内的数据流依赖关系通过将另一个数据流的唯一标识符作为父项引用进行声明;如果忽略标识符,相应数据流将依赖于“根数据流”

声明的数据流依赖下,首先向父节点分配资源,在向依赖项分配资源,比如先处理 D 的响应,在处理 C 的。而对于上图中右侧的两种情况,当具有相同父级的时候,自然是先处理父级,再处理依赖级,而依赖的的资源分配取决于其权重,如 C 下面的 A/B,其权重分别是 12 : 4,所以 A 将占有 3/4 的资源,而 B 占有剩下的 1/4 的资源。

通过优先级的处理,可以提高浏览器的性能。当然客户端也可以自己随时更新优先级,其可以 通过 HEADERS 帧里面的优先级 priority 属性,也可以通过 PRIORITY 帧来专门设立优先级

优先级的设立不一定能保证对端的遵守,非强制性需求,well,这也是必要行为,不能阻止服务端处理优先级低的资源。

流控制

流控制可以阻止发送方向接收方发送大量的数据,以免超过后者的需求或者处理能力,而同样的发送方忙碌的时候,可能仅仅能分配出少量资源给到高优先级的请求。

多路复用引入了资源的竞争,流控制可以保证流之间不会严重影响到彼此。流控制通过使用WINDOW_UPDATE帧实现,可作用于单个流以及整个的连接。

流的控制也有自己的原则,这就不阐述了。流使用 WINDOW_UPDATE 帧来做流量控制。接收方有流控制窗口,当流控制窗口允许的时候,可以恢复之前的传输。

流量控制是为解决线头阻塞问题,同时在资源约束情况下保护一些操作顺利进行,针对单个连接,某个流可能被阻塞或处理缓慢,但同时不会影响到其它流上正在传输的数据。

服务器推送

HTTP/2 让服务器可以将响应主动得 “推送” 到客户端,而不是以前简单的 “请求-响应” 模式。一个典型的场景就是当许多个资源的时候,客户端要逐个逐个的检查文档,遇到了才请求,而这个过程是由时间差的,为什么不一开始就让服务端发送所有的数据呢?尤其是一些内联的元素,提前让服务器推送是极好的。

服务器推送可以提前发送常规的资源,当然客户端也可以选择拒绝了,

所有服务器推送数据流都由 PUSH_PROMISE 帧发起,表明了服务器向客户端推送所述资源的意图,并且需要先于请求推送资源的响应数据传输。 使用 HTTP/2,客户端仍然完全掌控服务器推送的使用方式。客户端可以限制并行推送的数据流数量;调整初始的流控制窗口以控制在数据流首次打开时推送的数据量;或完全停用服务器推送。这些优先级在 HTTP/2 连接开始时通过 SETTINGS 帧传输,可能随时更新。

标头压缩

在 HTTP 请求中,当你不断发送类似的请求,比如都是 GET 请求,只有请求地址是不一样的,但是其他的信息,包括 cookie 什么的都是一样的,这个时候客户端还是会重复发送类似的请求头,尤其是一些图片加载。当有数个资源的时候还好,如果数量很多,将会很占用开销,尤其是对 cookie 的反复传输,冗余数据浪费了非常多的宽带。

HTTP/2 使用 HPACK 压缩格式压缩请求和响应标头元数据。其采用了哈夫曼编码表,同时要求请求方和响应方维护同一张见过的标头的索引表

HPACK 压缩上下文包含一个静态表和一个动态表。静态表指的是常见的 HTTP 标头的列表,包括 method、status 等等,而动态表初始是空的,根据值动态更新。通过采用哈夫曼编码将请求中的部分替换为静态表或动态表中已经存在的索引,从而实现标头压缩。

注:在 HTTP/2 中,请求和响应标头字段的定义保持不变,仅有一些微小的差异:所有标头字段名称均为小写,请求行现在拆分成各个 :method、:scheme、:authority 和 :path 伪标头字段。

协商部署

服务器和客户端如何识别 HTTP/2 ,如何让协议从之前的 HTTP/1.1 升级到 HTTP/2 呢, 难道要通过握手来通知客户端和服务端采用 HTTP/2 的方式?HTTP/2 的前身 SPDY 为了实现协议升级,在 TLS 上添加了 NPN(Next Protocol Negotiation),就是一个在 TLS 上面的扩展,服务器会通知客户端其所支持的协议,让客户端来选择。

后来在标准化制定时候,NPN 就演变成为 ALPN(Application Layer Protocal Negotiation)。ALPN 中是让客户端提供其所支持的协议类型,让服务端来选择。

这就要求 HTTP/2 是基于 HTTPS 的,也就是 HTTP 2 over HTTPS。那有没有不建立在 HTTPS 的 HTTP 2 呢?有的,但是主流浏览器都不支持的,这里就不做介绍了,看下图就好了。

左图中基于 HTTPS 的 HTTP/2,客户端在和服务端请求之前会在 TLS 中的通信中协商具体用什么协议。看看 wireshark 捕获到的数据:

上图中可以看出本机在与 183.61.14.105 通信前的 TLS 里面,有个 APLN 的插件,插件里面提供了两种协议,第一个是 h2,也就是 HTTP/2 的版本标识符,后面一个是 http/1.1 也就是之前的版本,如果服务端不支持 h2,就采用 http/1.1。在服务度的 Server Hello 里面返回的 ALPN 就只有 h2,所以当前协议就采用 HTTP/2 了。

参考

  1. HTTP2 is here, let's optimize! 大神的文章
  2. HTTP/2:新的机遇与挑战 大神的总结
  3. High Performance Browser Networking 书中自有黄金屋